/*
 * Copyright (C) 2007-2013 Geometer Plus <contact@geometerplus.com>
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

package org.geometerplus.zlibrary.text.view;

import android.graphics.Point;
import android.util.Log;

import org.geometerplus.fbreader.book.Bookmark;
import org.geometerplus.zlibrary.core.application.ZLApplication;
import org.geometerplus.zlibrary.core.filesystem.ZLFile;
import org.geometerplus.zlibrary.core.util.RationalNumber;
import org.geometerplus.zlibrary.core.util.ZLColor;
import org.geometerplus.zlibrary.core.view.ZLPaintContext;
import org.geometerplus.zlibrary.core.view.ZLView;
import org.geometerplus.zlibrary.text.hyphenation.ZLTextHyphenationInfo;
import org.geometerplus.zlibrary.text.hyphenation.ZLTextHyphenator;
import org.geometerplus.zlibrary.text.model.ZLTextAlignmentType;
import org.geometerplus.zlibrary.text.model.ZLTextMark;
import org.geometerplus.zlibrary.text.model.ZLTextModel;
import org.geometerplus.zlibrary.text.model.ZLTextParagraph;
import org.geometerplus.zlibrary.text.view.style.ZLTextStyleCollection;
import org.geometerplus.zlibrary.ui.android.view.LQReadingView;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeSet;

import logic.util.BookSetting;


public abstract class ZLTextView extends ZLTextViewBase {
    public static final int MAX_SELECTION_DISTANCE = 10;
    private static final int FIRST_GENERATED_PAGE_COUNT = 100;
    private static final int SECOND_GENERATED_PAGE_COUNT = 100;

    private ZLTextWordCursor myStartCursor;
    private ZLTextWordCursor myEndCursor;

    private Timer mTimer;
    private TimerTask makeTask;
    private boolean isMakeTaskCanceled = false;

    public interface ScrollingMode {
        int NO_OVERLAPPING = 0;
        int KEEP_LINES = 1;
        int SCROLL_LINES = 2;
        int SCROLL_PERCENTAGE = 3;
    }

    private ZLTextModel myModel;

    private interface SizeUnit {
        int PIXEL_UNIT = 0;
        int LINE_UNIT = 1;
    }

    private int myScrollingMode;
    private int myOverlappingValue;

    private ZLTextPage myCurrentPage = new ZLTextPage();
    private List<ZLTextWordCursor> myPages = new ArrayList<>();
    private int myCurrentPageIndex = 0;

    private final HashMap<ZLTextLineInfo, ZLTextLineInfo> myLineInfoCache = new HashMap<>();

    private ZLTextRegion.Soul mySelectedRegionSoul;
    private boolean myHighlightSelectedRegion = true;

    private final ZLTextSelection mySelection = new ZLTextSelection(this);
    private final Set<ZLTextHighlighting> myHighlightings = Collections.synchronizedSet(new TreeSet<ZLTextHighlighting>());
    //private ZLTextPage                                    tempPage                  = new ZLTextPage();
    private HashMap<Integer, ZLTextPage> myExistPages = new HashMap<>();

    public ZLTextView(ZLApplication application) {
        super(application);
    }

    public synchronized void setModel(ZLTextModel model) {
        ZLTextParagraphCursorCache.clear();

        mySelection.clear();
        myHighlightings.clear();
        //笔记末点位置
        mRemarkIconPositions.clear();

        myModel = model;
        if (myModel == null) {
            return;
        }
        myCurrentPageIndex = 0;
        generatePages();//生成所有的页面
        if (myPages.size() > 0) {
            myCurrentPage = getPage(PageIndex.current);
        }
        Application.getViewWidget().reset();
    }

    public ZLTextModel getModel() {
        return myModel;
    }

    public void generatePages() {
        myPages.clear();
        myExistPages.clear();
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
        if (makeTask != null) {
            makeTask.cancel();
            synchronized (makeTask) {
                makeTask = null;
            }
        }

        myStartCursor = new ZLTextWordCursor(ZLTextParagraphCursor.cursor(myModel, 0));
        int count = 0;
        do {
            ZLTextPage page = new ZLTextPage();
            page.PaintState = PaintStateEnum.START_IS_KNOWN;
            page.StartCursor.setCursor(myStartCursor);
            preparePaintInfo(page);
            myEndCursor = page.EndCursor;
            if (myEndCursor == null) {
                break;
            }
            myPages.add(myStartCursor);
            count++;
            myStartCursor = myEndCursor;
        }
        while (!myEndCursor.isEndOfText() && count < FIRST_GENERATED_PAGE_COUNT);

        if (!myStartCursor.isEndOfText() && count >= FIRST_GENERATED_PAGE_COUNT) {
            if (mTimer == null) {
                mTimer = new Timer();
            }
            makeTask = new TimerTask() {
                @Override
                public void run() {
                    generateRemainedPages();
                }
            };
            mTimer.schedule(makeTask, 500, 500);// 1秒后执行此任务, 每隔秒后重复执行
        }
    }

    private void generateRemainedPages() {
        List<ZLTextWordCursor> remainedPages = new ArrayList<>();
        int count = 0;
        try {
            do {
                ZLTextPage page = new ZLTextPage();
                page.PaintState = PaintStateEnum.START_IS_KNOWN;
                page.StartCursor.setCursor(myStartCursor);
                preparePaintInfo(page);
                myEndCursor = page.EndCursor;
                if (myEndCursor == null) {
                    break;
                }
                remainedPages.add(myStartCursor);
                count++;
                myStartCursor = myEndCursor;
            }
            while (!myEndCursor.isEndOfText() && count < SECOND_GENERATED_PAGE_COUNT);

        } catch (OutOfMemoryError e) {
            Log.e("生成页面", "生成页面时内存不足！");
        } catch (Exception e) {
            Log.e("生成页面", "生成页面错误！");
        }
        if (makeTask != null) {
            synchronized (myPages) {
                myPages.addAll(remainedPages);
            }
        }
        //请求重绘
        Application.getViewWidget().reset();
        Application.getViewWidget().repaint();

        remainedPages = null;//释放此对象

        if (myStartCursor.isEndOfText()) {
            mTimer.cancel();
        }
        //执行，以便引起真正的内存回收
        System.gc();
    }

    public void cancelGeneratePageTask() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }

        if (makeTask != null) {
            makeTask.cancel();
            synchronized (makeTask) {
                makeTask = null;
            }
        }
    }

    public int getCurrentPageIndex() {
        return myCurrentPageIndex;
    }

    public ZLTextWordCursor getStartCursor() {
        return myCurrentPage.StartCursor;
    }

    public ZLTextWordCursor getEndCursor() {
        return myCurrentPage.EndCursor;
    }

    public ZLTextWordCursor getLastCursorOfModel() {
        ZLTextParagraphCursor lastParagraph = ZLTextParagraphCursor.cursor(myModel, myModel.getParagraphsNumber() - 1);
        ZLTextWordCursor lastCursor = new ZLTextWordCursor(lastParagraph);
        lastCursor.moveToParagraphEnd();
        return lastCursor;
    }

    private synchronized void gotoMark(ZLTextMark mark) {
        if (mark == null) {
            return;
        }
        boolean doRepaint = false;

        if (myCurrentPage.StartCursor.isNull()) {
            return;
        }
        if (myCurrentPage.StartCursor.getParagraphIndex() != mark.ParagraphIndex || myCurrentPage.StartCursor.getMark().compareTo(mark) > 0) {
            doRepaint = true;
            ZLTextParagraphCursor paragraphCursor = ZLTextParagraphCursor.cursor(myModel, mark.ParagraphIndex);
            int eIndex = paragraphCursor.getElementIndex(mark.Offset);
            gotoPosition(mark.ParagraphIndex, eIndex, 0);
        }

        while (mark.compareTo(myCurrentPage.EndCursor.getMark()) > 0) {
            doRepaint = true;
            scrollPage(true, ScrollingMode.NO_OVERLAPPING, 0);
        }
        if (doRepaint) {
            Application.getViewWidget().reset();
            Application.getViewWidget().repaint();
        }
    }

    public synchronized void gotoHighlighting(ZLTextHighlighting highlighting) {
        boolean doRepaint = false;

        if (myCurrentPage.StartCursor.isNull()) {
            return;
        }
        if (!highlighting.intersects(myCurrentPage)) {
            gotoPosition(highlighting.getStartPosition().getParagraphIndex(), 0, 0);
        }

        while (!highlighting.intersects(myCurrentPage)) {
            doRepaint = true;
            scrollPage(true, ScrollingMode.NO_OVERLAPPING, 0);
        }
        if (doRepaint) {

            Application.getViewWidget().reset();
            Application.getViewWidget().repaint();
        }
    }

    public synchronized int search(final String text, boolean ignoreCase, boolean wholeText, boolean backward, boolean thisSectionOnly) {
        if (text.length() == 0) {
            return 0;
        }
        int startIndex = 0;
        int endIndex = myModel.getParagraphsNumber();
        if (thisSectionOnly) {
            // TODO: implement
        }
        int count = myModel.search(text, startIndex, endIndex, ignoreCase);
        if (count > 0) {
            rebuildPaintInfo();
            generatePages();
//			ZLTextMark mark = myCurrentPage.StartCursor.getMark();
//			gotoMark(wholeText ? (backward ? myModel.getLastMark() : myModel.getFirstMark()) : (backward ? myModel.getPreviousMark(mark) : myModel.getNextMark(mark)));
            Application.getViewWidget().reset();
            Application.getViewWidget().repaint();
        }

        return count;
    }

    public boolean canFindNext() {
        final ZLTextWordCursor end = myCurrentPage.EndCursor;
        return !end.isNull() && (myModel != null) && (myModel.getNextMark(end.getMark()) != null);
    }

    public synchronized void findNext() {
        final ZLTextWordCursor end = myCurrentPage.EndCursor;
        if (!end.isNull()) {
            gotoMark(myModel.getNextMark(end.getMark()));
        }
    }

    public boolean canFindPrevious() {
        final ZLTextWordCursor start = myCurrentPage.StartCursor;
        return !start.isNull() && (myModel != null) && (myModel.getPreviousMark(start.getMark()) != null);
    }

    public synchronized void findPrevious() {
        final ZLTextWordCursor start = myCurrentPage.StartCursor;
        if (!start.isNull()) {
            gotoMark(myModel.getPreviousMark(start.getMark()));
        }
    }

    public void clearFindResults() {
        if (!findResultsAreEmpty()) {
            myModel.removeAllMarks();
            rebuildPaintInfo();
            Application.getViewWidget().reset();
            Application.getViewWidget().repaint();
        }
    }

    public boolean findResultsAreEmpty() {
        return (myModel == null) || myModel.getMarks().isEmpty();
    }

    @Override
    public synchronized void onScrollingFinished(PageIndex pageIndex) {
        switch (pageIndex) {
            case current:
                break;
            case previous: {
                if (myCurrentPageIndex <= 0) {
                    break;
                }
                myCurrentPageIndex--;
                myCurrentPage = getPage(PageIndex.current);
                Application.getViewWidget().reset();
                break;
            }
            case next: {
                if (myCurrentPageIndex >= myPages.size() - 1) {
                    break;
                }
                myCurrentPageIndex++;
                myCurrentPage = getPage(PageIndex.current);
                Application.getViewWidget().reset();
                break;
            }
        }
    }

    public boolean removeHighlightings(Class<? extends ZLTextHighlighting> type) {
        boolean result = false;
        synchronized (myHighlightings) {
            for (Iterator<ZLTextHighlighting> it = myHighlightings.iterator(); it.hasNext(); ) {
                final ZLTextHighlighting h = it.next();
                if (type.isInstance(h)) {
                    it.remove();
                    result = true;
                }
            }
        }
        return result;
    }

    private HashMap<Bookmark, ZLTextPosition> mRemarkIconPositions = new HashMap<>();

    public void addRemarkIconsPosition(Bookmark bookmark, ZLTextPosition zlTextPosition) {
        mRemarkIconPositions.put(bookmark, zlTextPosition);
    }

    public void removeRemarkIconsPosition(Bookmark bookmark) {
        mRemarkIconPositions.remove(bookmark);
    }

    public void highlight(ZLTextPosition start, ZLTextPosition end) {
        removeHighlightings(ZLTextManualHighlighting.class);
        addHighlighting(new ZLTextManualHighlighting(this, start, end));
    }

    public final void addHighlighting(ZLTextHighlighting h) {
        myHighlightings.add(h);
        Application.getViewWidget().reset();
        Application.getViewWidget().repaint();
    }

    public final void addHighlightings(Collection<ZLTextHighlighting> hilites) {
        myHighlightings.addAll(hilites);
        Application.getViewWidget().reset();
        Application.getViewWidget().repaint();
    }

    public void clearHighlighting() {
        if (removeHighlightings(ZLTextManualHighlighting.class)) {
            Application.getViewWidget().reset();
            Application.getViewWidget().repaint();
        }
    }

    protected void moveSelectionCursorTo(ZLTextSelectionCursor cursor, int x, int y) {
        y -= ZLTextSelectionCursor.getHeight() / 2 + ZLTextSelectionCursor.getAccent() / 2;
        mySelection.setCursorInMovement(cursor, x, y);
        mySelection.expandTo(myCurrentPage, x, y);
        Application.getViewWidget().reset();
        Application.getViewWidget().repaint();
    }

    protected void releaseSelectionCursor() {
        mySelection.stop();
        Application.getViewWidget().reset();
        Application.getViewWidget().repaint();
    }

    protected ZLTextSelectionCursor getSelectionCursorInMovement() {
        return mySelection.getCursorInMovement();
    }

    private ZLTextSelection.Point getSelectionCursorPoint(ZLTextPage page, ZLTextSelectionCursor cursor) {
        if (cursor == ZLTextSelectionCursor.None) {
            return null;
        }

        if (cursor == mySelection.getCursorInMovement()) {
            return mySelection.getCursorInMovementPoint();
        }

        if (cursor == ZLTextSelectionCursor.Left) {
            if (mySelection.hasPartBeforePage(page)) {
                return null;
            }
            final ZLTextElementArea selectionStartArea = mySelection.getStartArea(page);
            if (selectionStartArea != null) {
                return new ZLTextSelection.Point(selectionStartArea.XStart, selectionStartArea.YEnd);
            }
        } else {
            if (mySelection.hasPartAfterPage(page)) {
                return null;
            }
            final ZLTextElementArea selectionEndArea = mySelection.getEndArea(page);
            if (selectionEndArea != null) {
                return new ZLTextSelection.Point(selectionEndArea.XEnd, selectionEndArea.YEnd);
            }
        }
        return null;
    }

    public ArrayList<ZLTextElementArea> getRemarkIconZLTextElementAreas(ZLView.PageIndex index) {
        ArrayList<ZLTextElementArea> areas = new ArrayList<>();
        ZLTextPage page = getPage(index);
        for (ZLTextPosition zlTextPosition : mRemarkIconPositions.values()) {
            boolean f = !page.StartCursor.isNull() && !page.EndCursor.isNull() && page.StartCursor.compareTo(zlTextPosition) < 0 && page.EndCursor.compareTo(zlTextPosition) > 0;
            if (f) {
                ZLTextElementArea area = page.TextElementMap.getLastBefore(zlTextPosition);
                if (area != null) areas.add(area);
            }
        }
        return areas;
    }

    private int distanceToCursor(int x, int y, ZLTextSelection.Point cursorPoint) {
        if (cursorPoint == null) {
            return Integer.MAX_VALUE;
        }

        final int dX, dY;

        final int w = ZLTextSelectionCursor.getWidth() / 2;
        if (x < cursorPoint.X - w) {
            dX = cursorPoint.X - w - x;
        } else if (x > cursorPoint.X + w) {
            dX = x - cursorPoint.X - w;
        } else {
            dX = 0;
        }

        final int h = ZLTextSelectionCursor.getHeight();
        if (y < cursorPoint.Y) {
            dY = cursorPoint.Y - y;
        } else if (y > cursorPoint.Y + h) {
            dY = y - cursorPoint.Y - h;
        } else {
            dY = 0;
        }

        return Math.max(dX, dY);
    }

    protected ZLTextSelectionCursor findSelectionCursor(int x, int y) {
        return findSelectionCursor(x, y, Integer.MAX_VALUE);
    }

    protected ZLTextSelectionCursor findSelectionCursor(int x, int y, int maxDistance) {
        if (mySelection.isEmpty()) {
            return ZLTextSelectionCursor.None;
        }

        final int leftDistance = distanceToCursor(x, y, getSelectionCursorPoint(myCurrentPage, ZLTextSelectionCursor.Left));
        final int rightDistance = distanceToCursor(x, y, getSelectionCursorPoint(myCurrentPage, ZLTextSelectionCursor.Right));

        if (rightDistance < leftDistance) {
            return rightDistance <= maxDistance ? ZLTextSelectionCursor.Right : ZLTextSelectionCursor.None;
        } else {
            return leftDistance <= maxDistance ? ZLTextSelectionCursor.Left : ZLTextSelectionCursor.None;
        }
    }

    private void drawSelectionCursor(ZLPaintContext context, ZLTextSelection.Point pt) {
        if (pt == null) {
            return;
        }

        final int w = ZLTextSelectionCursor.getWidth() / 6;
        final int h = ZLTextSelectionCursor.getHeight() / 2;
        final int a = ZLTextSelectionCursor.getAccent() / 2;
        final int[] xs = {
                pt.X + 2,
                pt.X + 2,
                pt.X - 2,
                pt.X - 2
        };
        final int[] ys = {
                pt.Y - h,
                pt.Y + a,
                pt.Y + a,
                pt.Y - h
        };
        context.setFillColor(getSelectionBackgroundColor(), 192);
        context.fillPolygon(xs, ys);
        //		context.setLineColor(context.getBackgroundColor());
        context.setLineColor(getSelectionBackgroundColor());
        context.drawPolygonalLine(xs, ys);
    }

    @Override
    public synchronized void preparePage(ZLPaintContext context, PageIndex pageIndex) {
        setContext(context);
    }

    @Override
    public synchronized void paint(ZLPaintContext context, PageIndex pageIndex) {
        setContext(context);
        final ZLFile wallpaper = getWallpaperFile();
        if (wallpaper != null) {
            context.clear(wallpaper, getWallpaperMode());
        } else {
            context.clear(getBackgroundColor());
        }

        if (myModel == null || myModel.getParagraphsNumber() == 0) {
            return;
        }

        ZLTextPage page = getPage(pageIndex);

        page.TextElementMap.clear();

        if (!page.isBuilded) {//没有build 此页， 所以需要build
            //page.PaintState = PaintStateEnum.START_IS_KNOWN;
            preparePaintInfo(page);
            page.isBuilded = true;
        }

        if (page.StartCursor.isNull() || page.EndCursor.isNull()) {
            page.isBuilded = false;
            return;
        }

        final ArrayList<ZLTextLineInfo> lineInfos = page.LineInfos;
        final int[] labels = new int[lineInfos.size() + 1];
        int x = getLeftMargin();
        int y = getTopMargin();
        int index = 0;
        for (ZLTextLineInfo info : lineInfos) {
            prepareTextLine(page, info, x, y);
            y += info.Height + info.Descent + info.VSpaceAfter;
            labels[++index] = page.TextElementMap.size();
            if (index == page.Column0Height) {
                y = getTopMargin();
                x += page.getTextWidth() + getSpaceBetweenColumns();
            }
        }

        x = getLeftMargin();
        y = getTopMargin();
        index = 0;
        boolean end = false;
        for (ZLTextLineInfo info : lineInfos) {
            drawHighlightings(page, info, labels[index], labels[index + 1], x, y, end);
            end = info.isEndOfParagraph();
            y += info.Height + info.Descent + info.VSpaceAfter;
            ++index;
            if (index == page.Column0Height) {
                y = getTopMargin();
                x += page.getTextWidth() + getSpaceBetweenColumns();
            }
        }

        x = getLeftMargin();
        y = getTopMargin();
        index = 0;
        for (ZLTextLineInfo info : lineInfos) {
            drawTextLine(page, info, labels[index], labels[index + 1]);
            y += info.Height + info.Descent + info.VSpaceAfter;
            ++index;
            if (index == page.Column0Height) {
                y = getTopMargin();
                x += page.getTextWidth() + getSpaceBetweenColumns();
            }
        }

        final ZLTextRegion selectedElementRegion = getSelectedRegion(page);
        if (selectedElementRegion != null && myHighlightSelectedRegion) {
            selectedElementRegion.draw(context);
        }

        drawSelectionCursor(context, getSelectionCursorPoint(page, ZLTextSelectionCursor.Left));
        drawSelectionCursor(context, getSelectionCursorPoint(page, ZLTextSelectionCursor.Right));
    }

    private ZLTextPage getCurrentPage() {
        myCurrentPage = myExistPages.get(myCurrentPageIndex);
        if (myCurrentPage == null) {
            myCurrentPage = new ZLTextPage();
            myCurrentPage.PaintState = PaintStateEnum.START_IS_KNOWN;
            myCurrentPage.StartCursor.setCursor(myPages.get(myCurrentPageIndex));
            myExistPages.put(myCurrentPageIndex, myCurrentPage);
        }
        return myCurrentPage;
    }

    private ZLTextPage getPage(PageIndex pageIndex) {
        if (myPages == null || myPages.size() == 0) {
            return new ZLTextPage();
        }

        switch (pageIndex) {
            default:
            case current:
                return getCurrentPage();
            case previous:
                if (myCurrentPageIndex <= 0) {
                    return getCurrentPage();
                } else {
                    int index = myCurrentPageIndex - 1;
                    ZLTextPage page = myExistPages.get(index);
                    if (page == null) {
                        page = new ZLTextPage();
                        page.PaintState = PaintStateEnum.START_IS_KNOWN;
                        page.StartCursor.setCursor(myPages.get(index));
                        myExistPages.put(index, page);
                    }
                    return page;
                    //return myPages.get(myCurrentPageIndex - 1);
                }
            case next:
                if (myCurrentPageIndex >= myPages.size() - 1) {
                    return getCurrentPage();
                } else {
                    int index = myCurrentPageIndex + 1;
                    ZLTextPage page = myExistPages.get(index);
                    if (page == null) {
                        page = new ZLTextPage();
                        page.PaintState = PaintStateEnum.START_IS_KNOWN;
                        page.StartCursor.setCursor(myPages.get(index));
                        myExistPages.put(index, page);
                    }
                    return page;
                }
        }
    }

    public static final int SCROLLBAR_HIDE = 0;
    public static final int SCROLLBAR_SHOW = 1;
    public static final int SCROLLBAR_SHOW_AS_PROGRESS = 2;

    public abstract int scrollbarType();

    @Override
    public final boolean isScrollbarShown() {
        return scrollbarType() == SCROLLBAR_SHOW || scrollbarType() == SCROLLBAR_SHOW_AS_PROGRESS;
    }

    protected final synchronized int sizeOfTextBeforeParagraph(int paragraphIndex) {
        return myModel != null ? myModel.getTextLength(paragraphIndex - 1) : 0;
    }

    public final synchronized int sizeOfFullText() {
        if (myModel == null || myModel.getParagraphsNumber() == 0) {
            return 1;
        }
        int size = myModel.getTextLength(myModel.getParagraphsNumber() - 1);
        //		// FBReaderApp.Log("sizeOfFullText() "+size);
        return size;
    }

    public final synchronized int getCurrentCharNumber(PageIndex pageIndex, boolean startNotEndOfPage) {
        if (myModel == null || myModel.getParagraphsNumber() == 0) {
            return 0;
        }
        final ZLTextPage page = getPage(pageIndex);
        if (startNotEndOfPage) {
            //			// FBReaderApp.Log("	if (startNotEndOfPage)");
            return Math.max(0, sizeOfTextBeforeCursor(page.StartCursor));
        } else {
            int end = sizeOfTextBeforeCursor(page.EndCursor);
            if (end == -1) {/* // FBReaderApp.Log("	if (startNotEndOfPage) else{}"); */
                end = myModel.getTextLength(myModel.getParagraphsNumber() - 1) - 1;
            }
            return Math.max(1, end);
        }
    }

    @Override
    public final synchronized int getScrollbarFullSize() {
        return sizeOfFullText();
    }

    @Override
    public final synchronized int getScrollbarThumbPosition(PageIndex pageIndex) {
        return scrollbarType() == SCROLLBAR_SHOW_AS_PROGRESS ? 0 : getCurrentCharNumber(pageIndex, true);
    }

    @Override
    public final synchronized int getScrollbarThumbLength(PageIndex pageIndex) {
        int start = scrollbarType() == SCROLLBAR_SHOW_AS_PROGRESS ? 0 : getCurrentCharNumber(pageIndex, true);
        int end = getCurrentCharNumber(pageIndex, false);
        return Math.max(1, end - start);
    }

    private int sizeOfTextBeforeCursor(ZLTextWordCursor wordCursor) {
        final ZLTextParagraphCursor paragraphCursor = wordCursor.getParagraphCursor();
        if (paragraphCursor == null) {
            return -1;
        }
        final int paragraphIndex = paragraphCursor.Index;
        int sizeOfText = myModel.getTextLength(paragraphIndex - 1);
        final int paragraphLength = paragraphCursor.getParagraphLength();
        if (paragraphLength > 0) {
            sizeOfText += (myModel.getTextLength(paragraphIndex) - sizeOfText) * wordCursor.getElementIndex() / paragraphLength;
        }
        return sizeOfText;
    }

    // Can be called only when (myModel.getParagraphsNumber() != 0)
    public synchronized float computeCharsPerPage() {
        setTextStyle(ZLTextStyleCollection.Instance().getBaseStyle());

        final int textWidth = getTextColumnWidth();
        final int textHeight = getTextAreaHeight();

        final int num = myModel.getParagraphsNumber();
        final int totalTextSize = myModel.getTextLength(num - 1);
        //		// FBReaderApp.Log("computeCharsPerPage() 总的字体数"+totalTextSize+" 行数  "+num);
        final float charsPerParagraph = ((float) totalTextSize) / num;

        final float charWidth = computeCharWidth();

        final int indentWidth = getElementWidth(ZLTextElement.Indent, 0);
        final float effectiveWidth = textWidth - (indentWidth + 0.5f * textWidth) / charsPerParagraph;
        float charsPerLine = Math.min(effectiveWidth / charWidth, charsPerParagraph * 1.2f);

        final int strHeight = getWordHeight() + getContext().getDescent();
        final int effectiveHeight = (int) (textHeight - (getTextStyle().getSpaceBefore() + getTextStyle().getSpaceAfter()) / charsPerParagraph);
        final int linesPerPage = effectiveHeight / strHeight;

        int ScreentTextSize = (int) charsPerLine;
        ScreentTextSize *= linesPerPage;
        //		// FBReaderApp.Log("computeCharsPerPage()  charsPerLine "+charsPerLine+" linesPerPage "+linesPerPage+" return "+ScreentTextSize);
        return ScreentTextSize;
    }

    public synchronized int computeTextPageNumber(int textSize) {
        if (myModel == null || myModel.getParagraphsNumber() == 0) {
            return 1;
        }

        final float factor = 1.0f / computeCharsPerPage();

        final int pages = (int) Math.ceil(textSize * factor); //(int)(textSize * factor);
        // FBReaderApp.Log("computeTextPageNumber() pages"+pages+" factor "+factor);
        return Math.max(pages, 1);
    }

    private static final char[] ourDefaultLetters = "System developers have used modeling languages for decades to specify, visualize, construct, and document systems. The Unified Modeling Language (UML) is one of those languages. UML makes it possible for team members to collaborate by providing a common language that applies to a multitude of different systems. Essentially, it enables you to communicate solutions in a consistent, tool-supported language.".toCharArray();

    private final char[] myLettersBuffer = new char[512];
    private int myLettersBufferLength = 0;
    private ZLTextModel myLettersModel = null;
    private float myCharWidth = -1f;

    private final float computeCharWidth() {
        if (myLettersModel != myModel) {
            //			// FBReaderApp.Log("computeCharWidth() ");
            myLettersModel = myModel;
            myLettersBufferLength = 0;
            myCharWidth = -1f;

            int paragraph = 0;
            final int textSize = myModel.getTextLength(myModel.getParagraphsNumber() - 1);
            if (textSize > myLettersBuffer.length) {
                paragraph = myModel.findParagraphByTextLength((textSize - myLettersBuffer.length) / 2);
            }
            while (paragraph < myModel.getParagraphsNumber() && myLettersBufferLength < myLettersBuffer.length) {
                ZLTextParagraph.EntryIterator it = myModel.getParagraph(paragraph++).iterator();
                while (it.hasNext() && myLettersBufferLength < myLettersBuffer.length) {
                    it.next();
                    if (it.getType() == ZLTextParagraph.Entry.TEXT) {
                        final int len = Math.min(it.getTextLength(), myLettersBuffer.length - myLettersBufferLength);
                        System.arraycopy(it.getTextData(), it.getTextOffset(), myLettersBuffer, myLettersBufferLength, len);
                        myLettersBufferLength += len;
                    }
                }
            }

            if (myLettersBufferLength == 0) {
                myLettersBufferLength = Math.min(myLettersBuffer.length, ourDefaultLetters.length);
                System.arraycopy(ourDefaultLetters, 0, myLettersBuffer, 0, myLettersBufferLength);
            }
        }

        if (myCharWidth < 0f) {
            myCharWidth = computeCharWidth(myLettersBuffer, myLettersBufferLength);
        }
        return myCharWidth;
    }

    private final float computeCharWidth(char[] pattern, int length) {
        return getContext().getStringWidth(pattern, 0, length) / ((float) length);
    }

    public static class PagePosition {
        public final int Current;
        public final int Total;

        PagePosition(int current, int total) {
            Current = current;
            Total = total;
        }
    }

    public static class PagePositionPercent {
        public static int CurrPage;
        public static int CountPage;
    }

    public final synchronized PagePosition pagePosition() {
        int current = myCurrentPageIndex + 1;
        int total = myPages.size();
        PagePositionPercent.CurrPage = current;
        PagePositionPercent.CountPage = total;
        if (total != 0) {
            current = Math.round(current * 100 / total);
        } else {
            current = 0;
        }
        total = 100;
        // FBReaderApp.Log("current "+current+" total "+total);
        return new PagePosition(current, total);
    }

    public final RationalNumber getProgress() {
        final PagePosition position = pagePosition();
        return RationalNumber.create(position.Current, position.Total);
    }

    public final synchronized void gotoPage(int page) {
        if (myModel == null || myModel.getParagraphsNumber() == 0) {
            return;
        }
        if (page < 0 || page >= myPages.size()) {
            return;
        }
        myCurrentPageIndex = page;
        myCurrentPage = getPage(PageIndex.current);
        Application.getViewWidget().reset();
    }

    public void gotoHome() {
        myCurrentPageIndex = 0;
        myCurrentPage = getPage(PageIndex.current);
        Application.getViewWidget().reset();
    }

    public void gotoLastPage() {
        myCurrentPageIndex = myPages.size() - 1;
        myCurrentPage = getPage(PageIndex.current);
        Application.getViewWidget().reset();
    }

    private void drawHighlightings(ZLTextPage page, ZLTextLineInfo info, int from, int to, int x, int y, boolean end) {
        if (from == to) {
            return;
        }

        final LinkedList<ZLTextHighlighting> hilites = new LinkedList<>();
        if (mySelection.intersects(page)) {
            hilites.add(mySelection);
        }
        synchronized (myHighlightings) {
            for (ZLTextHighlighting h : myHighlightings) {
                if (h.intersects(page)) {
                    hilites.add(h);
                }
            }
        }
        if (hilites.isEmpty()) {
            return;
        }

        final ZLTextElementArea fromArea = page.TextElementMap.get(from);
        final ZLTextElementArea toArea = page.TextElementMap.get(to - 1);
        for (ZLTextHighlighting h : hilites) {
            final ZLColor bgColor = h.getBackgroundColor();
            if (bgColor == null) {
                continue;
            }
            final ZLTextElementArea selectionStartArea = h.getStartArea(page);
            if (selectionStartArea == null || selectionStartArea.compareTo(toArea) > 0) {
                continue;
            }
            final ZLTextElementArea selectionEndArea = h.getEndArea(page);
            if (selectionEndArea == null || selectionEndArea.compareTo(fromArea) < 0) {
                continue;
            }

            final int top = y + 1;
            int left, right, bottom = y + info.Height + info.Descent;
            if (selectionStartArea.compareTo(fromArea) < 0) {
                if (end) {
                    left = fromArea.XStart + BookSetting.getFontSize() * 2;
                    //					// FBReaderApp.Log("新启一个段落");
                } else {
                    left = fromArea.XStart;
                    //					// FBReaderApp.Log("还是上个段落");
                }
            } else {
                left = selectionStartArea.XStart;
            }
            if (selectionEndArea.compareTo(toArea) > 0) {
                //				right = x + page.getTextWidth() - 1;
                right = toArea.XEnd;
                bottom += info.VSpaceAfter;
            } else {
                right = selectionEndArea.XEnd;
            }
            //			getContext().setFillColor(bgColor);
            //			getContext().fillRectangle(left, top, right, bottom);
            getContext().setLineColor(bgColor);
            getContext().setTextColor(getTextColor(ZLTextHyperlink.NO_LINK));
            getContext().drawLine(left, bottom, right, bottom);

        }
    }

    private static final char[] SPACE = new char[]{
            ' '
    };

    private void drawTextLine(ZLTextPage page, ZLTextLineInfo info, int from, int to) {
        final ZLPaintContext context = getContext();
        final ZLTextParagraphCursor paragraph = info.ParagraphCursor;
        int index = from;
        final int endElementIndex = info.EndElementIndex;
        int charIndex = info.RealStartCharIndex;
        int Index = 0;

        for (int wordIndex = info.RealStartElementIndex; wordIndex != endElementIndex && index < to; ++wordIndex, charIndex = 0) {

            final ZLTextElement element = paragraph.getElement(wordIndex);
            final ZLTextElementArea area = page.TextElementMap.get(index);
            if (element == area.Element) {
                ++index;
                if (area.ChangeStyle) {
                    setTextStyle(area.Style);
                }
                final int areaX = area.XStart;
                final int areaY = area.YEnd - getElementDescent(element) - getTextStyle().getVerticalShift();
                //总是执行这个
                if (element instanceof ZLTextWord) {
                    //					System.out.println(areaX+"  字的X坐标");
                    drawWord(areaX, areaY, (ZLTextWord) element, charIndex, -1, false, mySelection.isAreaSelected(area) ? getSelectionForegroundColor() : getTextColor(getTextStyle().Hyperlink));
                } else if (element instanceof ZLTextImageElement) {
                    final ZLTextImageElement imageElement = (ZLTextImageElement) element;
                    context.drawImage(areaX, areaY, imageElement.ImageData, getTextAreaSize(), getScalingType(imageElement));
                } else if (element == ZLTextElement.HSpace) {
                    final int cw = context.getSpaceWidth();
                    /*
					 * context.setFillColor(getHighlightingColor());
					 * context.fillRectangle(
					 * area.XStart, areaY - context.getStringHeight(),
					 * area.XEnd - 1, areaY + context.getDescent()
					 * );
					 */
                    for (int len = 0; len < area.XEnd - area.XStart; len += cw) {
                        context.drawString(areaX + len, areaY, SPACE, 0, 1);
                    }
                }
            }
        }
        if (index != to) {
            ZLTextElementArea area = page.TextElementMap.get(index++);
            if (area.ChangeStyle) {
                setTextStyle(area.Style);
            }
            final int start = info.StartElementIndex == info.EndElementIndex ? info.StartCharIndex : 0;
            final int len = info.EndCharIndex - start;
            final ZLTextWord word = (ZLTextWord) paragraph.getElement(info.EndElementIndex);
            System.out.println("index != to");
            drawWord(area.XStart, area.YEnd - context.getDescent() - getTextStyle().getVerticalShift(), word, start, len, area.AddHyphenationSign, mySelection.isAreaSelected(area) ? getSelectionForegroundColor() : getTextColor(getTextStyle().Hyperlink));
        }
    }

    private void buildInfos(ZLTextPage page, ZLTextWordCursor start, ZLTextWordCursor result) {
        result.setCursor(start);
        int textAreaHeight = page.getTextHeight();
        page.LineInfos.clear();
        page.Column0Height = 0;
        boolean nextParagraph;
        do {
            resetTextStyle();
            final ZLTextParagraphCursor paragraphCursor = result.getParagraphCursor();
            final int wordIndex = result.getElementIndex();
            applyStyleChanges(paragraphCursor, 0, wordIndex);
            ZLTextLineInfo info = new ZLTextLineInfo(paragraphCursor, wordIndex, result.getCharIndex(), getTextStyle());
            final int endIndex = info.ParagraphCursorLength;
            while (info.EndElementIndex != endIndex) {
                info = processTextLine(page, paragraphCursor, info.EndElementIndex, info.EndCharIndex, endIndex);
                textAreaHeight -= info.Height + info.Descent;
                if (textAreaHeight < 0 && page.LineInfos.size() > page.Column0Height) {
                    if (page.Column0Height == 0 && page.twoColumnView()) {
                        textAreaHeight = page.getTextHeight();
                        textAreaHeight -= info.Height + info.Descent;
                        page.Column0Height = page.LineInfos.size();
                    } else {
                        break;
                    }
                }
                textAreaHeight -= info.VSpaceAfter;
                result.moveTo(info.EndElementIndex, info.EndCharIndex);
                page.LineInfos.add(info);
                if (textAreaHeight < 0) {
                    if (page.Column0Height == 0 && page.twoColumnView()) {
                        textAreaHeight = page.getTextHeight();
                        page.Column0Height = page.LineInfos.size();
                    } else {
                        break;
                    }
                }
            }
            nextParagraph = result.isEndOfParagraph() && result.nextParagraph();
            if (nextParagraph && result.getParagraphCursor().isEndOfSection()) {
                if (page.Column0Height == 0 && page.twoColumnView() && !page.LineInfos.isEmpty()) {
                    textAreaHeight = page.getTextHeight();
                    page.Column0Height = page.LineInfos.size();
                }
            }
        }
        while (nextParagraph && textAreaHeight >= 0 && (!result.getParagraphCursor().isEndOfSection() || page.LineInfos.size() == page.Column0Height));
        resetTextStyle();
    }

    private boolean isHyphenationPossible() {
        return ZLTextStyleCollection.Instance().getBaseStyle().AutoHyphenationOption.getValue() && getTextStyle().allowHyphenations();
    }

    private ZLTextLineInfo processTextLine(ZLTextPage page, ZLTextParagraphCursor paragraphCursor, final int startIndex, final int startCharIndex, final int endIndex) {
        final ZLPaintContext context = getContext();
        final ZLTextLineInfo info = new ZLTextLineInfo(paragraphCursor, startIndex, startCharIndex, getTextStyle());
        final ZLTextLineInfo cachedInfo = myLineInfoCache.get(info);
        if (cachedInfo != null) {
            applyStyleChanges(paragraphCursor, startIndex, cachedInfo.EndElementIndex);
            return cachedInfo;
        }

        int currentElementIndex = startIndex;
        int currentCharIndex = startCharIndex;
        final boolean isFirstLine = startIndex == 0 && startCharIndex == 0;

        boolean isFirstLineOfChapter = false;
        if (isFirstLine) {
            if (paragraphCursor.Index == 0) {
                isFirstLineOfChapter = true;
            } else if (paragraphCursor.Index == 1) {
                if (myModel.getTextLength(paragraphCursor.Index - 1) == 0) {
                    isFirstLineOfChapter = true;
                }
            }
        }

        if (isFirstLine) {
            ZLTextElement element = paragraphCursor.getElement(currentElementIndex);
            while (isStyleChangeElement(element)) {
                applyStyleChangeElement(element);
                ++currentElementIndex;
                currentCharIndex = 0;
                if (currentElementIndex == endIndex) {
                    break;
                }
                element = paragraphCursor.getElement(currentElementIndex);
            }
            info.StartStyle = getTextStyle();
            info.RealStartElementIndex = currentElementIndex;
            info.RealStartCharIndex = currentCharIndex;
        }

        ZLTextStyle storedStyle = getTextStyle();

        info.LeftIndent = getTextStyle().getLeftIndent();
        if (isFirstLine) {
            info.LeftIndent += getTextStyle().getFirstLineIndentDelta();
            info.Height += getTextStyle().getFirstLineSpacePercent();
        }

        if (isFirstLineOfChapter) {
            int twoWidth = context.getStringWidth("您好");
            info.LeftIndent += twoWidth;
        }

        info.Width = info.LeftIndent;

        if (info.RealStartElementIndex == endIndex) {
            info.EndElementIndex = info.RealStartElementIndex;
            info.EndCharIndex = info.RealStartCharIndex;
            return info;
        }

        int newWidth = info.Width;
        int newHeight = info.Height;
        int newDescent = info.Descent;
        int maxWidth = page.getTextWidth();
        boolean wordOccurred = false;
        boolean isVisible = false;
        int lastSpaceWidth = 0;
        int internalSpaceCounter = 0;
        boolean removeLastSpace = false;

        do {
            ZLTextElement element = paragraphCursor.getElement(currentElementIndex);
            newWidth += getElementWidth(element, currentCharIndex);
            newHeight = Math.max(newHeight, getElementHeight(element));
            newDescent = Math.max(newDescent, getElementDescent(element));
            if (element == ZLTextElement.HSpace) {
                if (wordOccurred) {
                    wordOccurred = false;
                    internalSpaceCounter++;
                    lastSpaceWidth = context.getSpaceWidth();
                    newWidth += lastSpaceWidth;
                }
            } else if (element instanceof ZLTextWord) {
                wordOccurred = true;
                isVisible = true;
            } else if (element instanceof ZLTextImageElement) {
                wordOccurred = true;
                isVisible = true;
            } else if (isStyleChangeElement(element)) {
                applyStyleChangeElement(element);
            }
            if (newWidth > maxWidth) {
                if (info.EndElementIndex != startIndex || element instanceof ZLTextWord) {
                    break;
                }
            }
            ZLTextElement previousElement = element;
            ++currentElementIndex;
            currentCharIndex = 0;
            boolean allowBreak = currentElementIndex == endIndex;
            if (!allowBreak) {
                element = paragraphCursor.getElement(currentElementIndex);
                allowBreak = ((!(element instanceof ZLTextWord) || previousElement instanceof ZLTextWord) && !(element instanceof ZLTextImageElement) && !(element instanceof ZLTextControlElement));
            }
            if (allowBreak) {
                info.IsVisible = isVisible;
                info.Width = newWidth;
                if (info.Height < newHeight) {
                    info.Height = newHeight;
                }
                if (info.Descent < newDescent) {
                    info.Descent = newDescent;
                }
                info.EndElementIndex = currentElementIndex;
                info.EndCharIndex = currentCharIndex;
                info.SpaceCounter = internalSpaceCounter;
                storedStyle = getTextStyle();
                removeLastSpace = !wordOccurred && (internalSpaceCounter > 0);
            }
        }
        while (currentElementIndex != endIndex);

        if (currentElementIndex != endIndex && (isHyphenationPossible() || info.EndElementIndex == startIndex)) {
            ZLTextElement element = paragraphCursor.getElement(currentElementIndex);
            if (element instanceof ZLTextWord) {
                final ZLTextWord word = (ZLTextWord) element;
                newWidth -= getWordWidth(word, currentCharIndex);
                int spaceLeft = maxWidth - newWidth;
                if ((word.Length > 3 && spaceLeft > 2 * context.getSpaceWidth()) || info.EndElementIndex == startIndex) {
                    ZLTextHyphenationInfo hyphenationInfo = ZLTextHyphenator.Instance().getInfo(word);
                    int hyphenationPosition = word.Length - 1;
                    int subwordWidth = 0;
                    for (; hyphenationPosition > currentCharIndex; hyphenationPosition--) {
                        if (hyphenationInfo.isHyphenationPossible(hyphenationPosition)) {
                            subwordWidth = getWordWidth(word, currentCharIndex, hyphenationPosition - currentCharIndex, word.Data[word.Offset + hyphenationPosition - 1] != '-');
                            if (subwordWidth <= spaceLeft) {
                                break;
                            }
                        }
                    }
                    if (hyphenationPosition == currentCharIndex && info.EndElementIndex == startIndex) {
                        hyphenationPosition = word.Length == currentCharIndex + 1 ? word.Length : word.Length - 1;
                        subwordWidth = getWordWidth(word, currentCharIndex, word.Length - currentCharIndex, false);
                        for (; hyphenationPosition > currentCharIndex + 1; hyphenationPosition--) {
                            subwordWidth = getWordWidth(word, currentCharIndex, hyphenationPosition - currentCharIndex, word.Data[word.Offset + hyphenationPosition - 1] != '-');
                            if (subwordWidth <= spaceLeft) {
                                break;
                            }
                        }
                    }
                    if (hyphenationPosition > currentCharIndex) {
                        info.IsVisible = true;
                        info.Width = newWidth + subwordWidth;
                        if (info.Height < newHeight) {
                            info.Height = newHeight;
                        }
                        if (info.Descent < newDescent) {
                            info.Descent = newDescent;
                        }
                        info.EndElementIndex = currentElementIndex;
                        info.EndCharIndex = hyphenationPosition;
                        info.SpaceCounter = internalSpaceCounter;
                        storedStyle = getTextStyle();
                        removeLastSpace = false;
                    }
                }
            }
        }

        if (removeLastSpace) {
            info.Width -= lastSpaceWidth;
            info.SpaceCounter--;
        }

        setTextStyle(storedStyle);

        if (isFirstLine) {
            info.Height += info.StartStyle.getSpaceBefore();
        }
        if (info.isEndOfParagraph()) {
            info.VSpaceAfter = getTextStyle().getSpaceAfter();
        }

        if (info.EndElementIndex != endIndex || endIndex == info.ParagraphCursorLength) {
            myLineInfoCache.put(info, info);
        }

        return info;
    }

    private void prepareTextLine(ZLTextPage page, ZLTextLineInfo info, int x, int y) {
        y = Math.min(y + info.Height, getTopMargin() + page.getTextHeight() - 1);

        final ZLPaintContext context = getContext();
        final ZLTextParagraphCursor paragraphCursor = info.ParagraphCursor;

        setTextStyle(info.StartStyle);
        int spaceCounter = info.SpaceCounter;
        int fullCorrection = 0;
        final boolean endOfParagraph = info.isEndOfParagraph();
        boolean wordOccurred = false;
        boolean changeStyle = true;
        x += info.LeftIndent;

        final int maxWidth = page.getTextWidth();
        switch (getTextStyle().getAlignment()) {
            case ZLTextAlignmentType.ALIGN_RIGHT:
                x += maxWidth - getTextStyle().getRightIndent() - info.Width;
                break;
            case ZLTextAlignmentType.ALIGN_CENTER:
                x += (maxWidth - getTextStyle().getRightIndent() - info.Width) / 2;
                break;
            case ZLTextAlignmentType.ALIGN_JUSTIFY:
                if (!endOfParagraph && (paragraphCursor.getElement(info.EndElementIndex) != ZLTextElement.AfterParagraph)) {
                    fullCorrection = maxWidth - getTextStyle().getRightIndent() - info.Width;
                }
                break;
            case ZLTextAlignmentType.ALIGN_LEFT:
            case ZLTextAlignmentType.ALIGN_UNDEFINED:
                // FBReaderApp.Log("ZLTextAlignmentType.ALIGN_LEFT:");
                break;
        }

        final ZLTextParagraphCursor paragraph = info.ParagraphCursor;
        final int paragraphIndex = paragraph.Index;
        final int endElementIndex = info.EndElementIndex;
        int charIndex = info.RealStartCharIndex;
        ZLTextElementArea spaceElement = null;
        List<Integer> list = new ArrayList<>();
        String xxx = x + "";
        int xx = Integer.valueOf(xxx);
        if (!endOfParagraph) {
            for (int wordIndex = info.RealStartElementIndex; wordIndex != endElementIndex; ++wordIndex, charIndex = 0) {
                final ZLTextElement element = paragraph.getElement(wordIndex);
                final int width = getElementWidth(element, charIndex);
                list.add(width + xx);
                xx += width;
            }
            int remain = maxWidth - list.get(list.size() - 1) + BookSetting.getMarginVlaue();
            for (int i = 0; i < list.size(); i++) {
                list.set(i, 0);
            }

            while (remain != 0) {
                for (int i = 1; i < list.size(); i++) {
                    if (remain > 0) {
                        list.set(i, list.get(i) + 1);
                    } else {
                        break;
                    }
                    remain--;
                }
            }

        }
        for (int wordIndex = info.RealStartElementIndex, index = 0; wordIndex != endElementIndex; ++wordIndex, charIndex = 0, index++) {
            final ZLTextElement element = paragraph.getElement(wordIndex);
            final int width = getElementWidth(element, charIndex);
            if (element == ZLTextElement.HSpace) {// FBReaderApp.Log("element == ZLTextElement.HSpace");
                if (wordOccurred && (spaceCounter > 0)) {
                    final int correction = fullCorrection / spaceCounter;
                    final int spaceLength = context.getSpaceWidth() + correction;
                    if (getTextStyle().isUnderline()) {
                        spaceElement = new ZLTextElementArea(paragraphIndex, wordIndex, 0, 0, // length
                                true, // is last in element
                                false, // add hyphenation sign
                                false, // changed style
                                getTextStyle(), element, x, x + spaceLength, y, y);
                    } else {
                        spaceElement = null;
                    }
                    x += spaceLength;
                    fullCorrection -= correction;
                    wordOccurred = false;
                    --spaceCounter;
                }
            } else if (element instanceof ZLTextWord || element instanceof ZLTextImageElement) {
                //			// FBReaderApp.Log("element instanceof ZLTextWord || element instanceof ZLTextImageElement");
                final int height = getElementHeight(element);
                final int descent = getElementDescent(element);
                final int length = element instanceof ZLTextWord ? ((ZLTextWord) element).Length : 0;
                if (spaceElement != null) {
                    // FBReaderApp.Log("spaceElement != null");
                    page.TextElementMap.add(spaceElement);
                    spaceElement = null;
                }
                if (!endOfParagraph) {
                    x += list.get(index);
                }

                page.TextElementMap.add(new ZLTextElementArea(paragraphIndex, wordIndex, charIndex, length - charIndex, true, // is last in element
                        false, // add hyphenation sign
                        changeStyle, getTextStyle(), element, x, x + width - 1, y - height + 1, y + descent));
                changeStyle = false;
                wordOccurred = true;

            } else if (isStyleChangeElement(element)) {
                applyStyleChangeElement(element);
                changeStyle = true;
            }
            x += width;
        }
        if (!endOfParagraph) {
            final int len = info.EndCharIndex;
            if (len > 0) {
                final int wordIndex = info.EndElementIndex;
                final ZLTextWord word = (ZLTextWord) paragraph.getElement(wordIndex);
                final boolean addHyphenationSign = word.Data[word.Offset + len - 1] != '-';
                final int width = getWordWidth(word, 0, len, addHyphenationSign);
                final int height = getElementHeight(word);
                final int descent = context.getDescent();
                page.TextElementMap.add(new ZLTextElementArea(paragraphIndex, wordIndex, 0, len, false, // is last in element
                        addHyphenationSign, changeStyle, getTextStyle(), word, x, x + width - 1, y - height + 1, y + descent));
            }
        }
    }

    public synchronized final void scrollPage(boolean forward, int scrollingMode, int value) {
        if (myCurrentPage.PaintState == PaintStateEnum.READY) {
            myCurrentPage.PaintState = forward ? PaintStateEnum.TO_SCROLL_FORWARD : PaintStateEnum.TO_SCROLL_BACKWARD;
            myScrollingMode = scrollingMode;
            myOverlappingValue = value;
        }
    }

    public final synchronized void gotoPosition(ZLTextPosition position) {
        if (position != null) {
            gotoPosition(position.getParagraphIndex(), position.getElementIndex(), position.getCharIndex());
        }
    }

    public final synchronized void gotoPosition(int paragraphIndex, int wordIndex, int charIndex) {
        if (myModel != null && myModel.getParagraphsNumber() > 0) {
            int pageindex = findPage(paragraphIndex, wordIndex, charIndex);
            gotoPage(pageindex);
        }
    }

    public final synchronized int findPage(int paragraphIndex, int wordIndex, int charIndex) {
        int pageIndex = -1;//没有找到
        int i = 0;
        for (; i < myPages.size(); i++) {
            ZLTextWordCursor cursor = myPages.get(i);
            int spIndex = cursor.getParagraphIndex();
            if (i == (myPages.size() - 1)) {
                cursor = getLastCursorOfModel();
            } else {
                cursor = myPages.get(i + 1);
            }
            int epIndex = cursor.getParagraphIndex();
            if (paragraphIndex >= spIndex) {
                if (paragraphIndex < epIndex) {
                    return i;
                } else if (paragraphIndex == epIndex && wordIndex < cursor.getElementIndex()) {
                    return i;
                }
            }
        }

        return pageIndex;
    }

    protected synchronized void preparePaintInfo() {
//		myPreviousPage.reset();
//		myNextPage.reset();
//		preparePaintInfo(myCurrentPage);
    }

    private synchronized void preparePaintInfo(ZLTextPage page) {

        page.setSize(getTextColumnWidth(), getTextAreaHeight(), twoColumnView(), false);

        if (page.PaintState == PaintStateEnum.NOTHING_TO_PAINT || page.PaintState == PaintStateEnum.READY) {
            return;
        }
        final int oldState = page.PaintState;

        final HashMap<ZLTextLineInfo, ZLTextLineInfo> cache = myLineInfoCache;
        for (ZLTextLineInfo info : page.LineInfos) {
            cache.put(info, info);
        }

        switch (page.PaintState) {
            default:
                break;
            case PaintStateEnum.TO_SCROLL_FORWARD:
                System.out.println("PaintStateEnum.TO_SCROLL_FORWARD");
                if (!page.EndCursor.isEndOfText()) {
                    final ZLTextWordCursor startCursor = new ZLTextWordCursor();
                    switch (myScrollingMode) {
                        case ScrollingMode.NO_OVERLAPPING:
                            break;
                        case ScrollingMode.KEEP_LINES:
                            page.findLineFromEnd(startCursor, myOverlappingValue);
                            break;
                        case ScrollingMode.SCROLL_LINES:
                            page.findLineFromStart(startCursor, myOverlappingValue);
                            if (startCursor.isEndOfParagraph()) {
                                startCursor.nextParagraph();
                            }
                            break;
                        case ScrollingMode.SCROLL_PERCENTAGE:
                            page.findPercentFromStart(startCursor, myOverlappingValue);
                            break;
                    }

                    if (!startCursor.isNull() && startCursor.samePositionAs(page.StartCursor)) {
                        page.findLineFromStart(startCursor, 1);
                    }

                    if (!startCursor.isNull()) {
                        final ZLTextWordCursor endCursor = new ZLTextWordCursor();
                        buildInfos(page, startCursor, endCursor);
                        if (!page.isEmptyPage() && (myScrollingMode != ScrollingMode.KEEP_LINES || !endCursor.samePositionAs(page.EndCursor))) {
                            page.StartCursor.setCursor(startCursor);
                            page.EndCursor.setCursor(endCursor);
                            break;
                        }
                    }

                    page.StartCursor.setCursor(page.EndCursor);
                    buildInfos(page, page.StartCursor, page.EndCursor);
                }
                break;
            case PaintStateEnum.TO_SCROLL_BACKWARD:
                System.out.println("PaintStateEnum.TO_SCROLL_BACKWARD:");
                if (!page.StartCursor.isStartOfText()) {
                    switch (myScrollingMode) {
                        case ScrollingMode.NO_OVERLAPPING:
                            page.StartCursor.setCursor(findStartOfPrevousPage(page, page.StartCursor));
                            break;
                        case ScrollingMode.KEEP_LINES: {
                            ZLTextWordCursor endCursor = new ZLTextWordCursor();
                            page.findLineFromStart(endCursor, myOverlappingValue);
                            if (!endCursor.isNull() && endCursor.samePositionAs(page.EndCursor)) {
                                page.findLineFromEnd(endCursor, 1);
                            }
                            if (!endCursor.isNull()) {
                                ZLTextWordCursor startCursor = findStartOfPrevousPage(page, endCursor);
                                if (startCursor.samePositionAs(page.StartCursor)) {
                                    page.StartCursor.setCursor(findStartOfPrevousPage(page, page.StartCursor));
                                } else {
                                    page.StartCursor.setCursor(startCursor);
                                }
                            } else {
                                page.StartCursor.setCursor(findStartOfPrevousPage(page, page.StartCursor));
                            }
                            break;
                        }
                        case ScrollingMode.SCROLL_LINES:
                            page.StartCursor.setCursor(findStart(page, page.StartCursor, SizeUnit.LINE_UNIT, myOverlappingValue));
                            break;
                        case ScrollingMode.SCROLL_PERCENTAGE:
                            page.StartCursor.setCursor(findStart(page, page.StartCursor, SizeUnit.PIXEL_UNIT, page.getTextHeight() * myOverlappingValue / 100));
                            break;
                    }
                    buildInfos(page, page.StartCursor, page.EndCursor);
                    if (page.isEmptyPage()) {
                        page.StartCursor.setCursor(findStart(page, page.StartCursor, SizeUnit.LINE_UNIT, 1));
                        buildInfos(page, page.StartCursor, page.EndCursor);
                    }
                }
                break;
            case PaintStateEnum.START_IS_KNOWN:
                System.out.println("START_IS_KNOWN");
                if (!page.StartCursor.isNull()) {
                    buildInfos(page, page.StartCursor, page.EndCursor);
                }
                break;
            case PaintStateEnum.END_IS_KNOWN:
                System.out.println("END_IS_KNOWN");
                if (!page.EndCursor.isNull()) {
                    page.StartCursor.setCursor(findStartOfPrevousPage(page, page.EndCursor));
                    buildInfos(page, page.StartCursor, page.EndCursor);
                }
                break;
        }
        page.PaintState = PaintStateEnum.READY;
        // TODO: cache?
        myLineInfoCache.clear();

    }

    public void clearCaches() {
        resetMetrics();
        rebuildPaintInfo();
        Application.getViewWidget().reset();
        myCharWidth = -1;
    }

    protected void rebuildPaintInfo() {
        ZLTextParagraphCursorCache.clear();
        myLineInfoCache.clear();
    }

    private int infoSize(ZLTextLineInfo info, int unit) {
        int info_size = (unit == SizeUnit.PIXEL_UNIT) ? (info.Height + info.Descent + info.VSpaceAfter) : (info.IsVisible ? 1 : 0);
        return info_size;
    }

    private int paragraphSize(ZLTextPage page, ZLTextWordCursor cursor, boolean beforeCurrentPosition, int unit) {
        final ZLTextParagraphCursor paragraphCursor = cursor.getParagraphCursor();
        if (paragraphCursor == null) {
            return 0;
        }
        final int endElementIndex = beforeCurrentPosition ? cursor.getElementIndex() : paragraphCursor.getParagraphLength();

        resetTextStyle();

        int size = 0;

        int wordIndex = 0;
        int charIndex = 0;
        while (wordIndex != endElementIndex) {
            ZLTextLineInfo info = processTextLine(page, paragraphCursor, wordIndex, charIndex, endElementIndex);
            wordIndex = info.EndElementIndex;
            charIndex = info.EndCharIndex;
            size += infoSize(info, unit);
        }
        //		// FBReaderApp.Log("paragraphSize()  "+size);
        return size;
    }

    private void skip(ZLTextPage page, ZLTextWordCursor cursor, int unit, int size) {
        final ZLTextParagraphCursor paragraphCursor = cursor.getParagraphCursor();
        if (paragraphCursor == null) {
            return;
        }
        final int endElementIndex = paragraphCursor.getParagraphLength();

        resetTextStyle();
        applyStyleChanges(paragraphCursor, 0, cursor.getElementIndex());

        while (!cursor.isEndOfParagraph() && (size > 0)) {
            ZLTextLineInfo info = processTextLine(page, paragraphCursor, cursor.getElementIndex(), cursor.getCharIndex(), endElementIndex);
            cursor.moveTo(info.EndElementIndex, info.EndCharIndex);
            size -= infoSize(info, unit);
        }
    }

    private ZLTextWordCursor findStartOfPrevousPage(ZLTextPage page, ZLTextWordCursor end) {
        if (twoColumnView()) {
            end = findStart(page, end, SizeUnit.PIXEL_UNIT, page.getTextHeight());
        }
        end = findStart(page, end, SizeUnit.PIXEL_UNIT, page.getTextHeight());
        return end;
    }

    private ZLTextWordCursor findStart(ZLTextPage page, ZLTextWordCursor end, int unit, int size) {
        final ZLTextWordCursor start = new ZLTextWordCursor(end);
        size -= paragraphSize(page, start, true, unit);
        boolean positionChanged = !start.isStartOfParagraph();
        start.moveToParagraphStart();
        while (size > 0) {
            if (positionChanged && start.getParagraphCursor().isEndOfSection()) {
                break;
            }
            if (!start.previousParagraph()) {
                break;
            }
            if (!start.getParagraphCursor().isEndOfSection()) {
                positionChanged = true;
            }
            size -= paragraphSize(page, start, false, unit);
        }
        skip(page, start, unit, -size);

        if (unit == SizeUnit.PIXEL_UNIT) {
            boolean sameStart = start.samePositionAs(end);
            if (!sameStart && start.isEndOfParagraph() && end.isStartOfParagraph()) {
                ZLTextWordCursor startCopy = new ZLTextWordCursor(start);
                startCopy.nextParagraph();
                sameStart = startCopy.samePositionAs(end);
            }
            if (sameStart) {
                start.setCursor(findStart(page, end, SizeUnit.LINE_UNIT, 1));
            }
        }

        return start;
    }

    protected ZLTextElementArea getElementByCoordinates(int x, int y) {
        return myCurrentPage.TextElementMap.binarySearch(x, y);
    }

    public void hideSelectedRegionBorder() {
        myHighlightSelectedRegion = false;
        Application.getViewWidget().reset();
    }

    private ZLTextRegion getSelectedRegion(ZLTextPage page) {
        return page.TextElementMap.getRegion(mySelectedRegionSoul);
    }

    public ZLTextRegion getSelectedRegion() {
        return getSelectedRegion(myCurrentPage);
    }

    protected ZLTextHighlighting findHighlighting(int x, int y, int maxDistance) {
        final ZLTextRegion region = findRegion(x, y, maxDistance, ZLTextRegion.AnyRegionFilter);
        if (region == null) {
            return null;
        }
        synchronized (myHighlightings) {
            for (ZLTextHighlighting h : myHighlightings) {
                if (h.getBackgroundColor() != null && h.intersects(region)) {
                    return h;
                }
            }
        }
        return null;
    }

    protected Bookmark findNoteIcon(int x, int y, Point point) {
        ZLTextPage page = getPage(PageIndex.current);
        synchronized (mRemarkIconPositions) {
            for (Bookmark bookmark : mRemarkIconPositions.keySet()) {
                ZLTextPosition textPosition = mRemarkIconPositions.get(bookmark);
                boolean f = !page.StartCursor.isNull() && !page.EndCursor.isNull() && page.StartCursor.compareTo(textPosition) < 0 && page.EndCursor.compareTo(textPosition) > 0;
                if (f) {
                    ZLTextElementArea area = page.TextElementMap.getLastBefore(textPosition);
                    boolean isInSide = (x >= area.XEnd - 10 && x <= area.XEnd + LQReadingView.ICON_WIDTH + 20) && (y >= area.YEnd - 10 && y <= area.YEnd + LQReadingView.ICON_HEIGHT + 40);
                    if (isInSide) {
                        point.x = area.XEnd + LQReadingView.ICON_WIDTH / 2;
                        point.y = area.YEnd;
                        return bookmark;
                    }
                }
            }
        }
        return null;
    }

    protected ZLTextRegion findRegion(int x, int y, ZLTextRegion.Filter filter) {
        return findRegion(x, y, Integer.MAX_VALUE - 1, filter);
    }

    protected ZLTextRegion findRegion(int x, int y, int maxDistance, ZLTextRegion.Filter filter) {
        return myCurrentPage.TextElementMap.findRegion(x, y, maxDistance, filter);
    }

    public void selectRegion(ZLTextRegion region) {
        final ZLTextRegion.Soul soul = region != null ? region.getSoul() : null;
        if (soul == null || !soul.equals(mySelectedRegionSoul)) {
            myHighlightSelectedRegion = true;
        }
        mySelectedRegionSoul = soul;
    }

    protected boolean initSelection(int x, int y) {
        y -= ZLTextSelectionCursor.getHeight() / 2 + ZLTextSelectionCursor.getAccent() / 2;
        if (!mySelection.start(x, y)) {
            return false;
        }
        Application.getViewWidget().reset();
        Application.getViewWidget().repaint();
        return true;
    }

    public void clearSelection() {
        if (mySelection.clear()) {
            Application.getViewWidget().reset();
            Application.getViewWidget().repaint();
        }
    }

    public int getSelectionStartY() {
        if (mySelection.isEmpty()) {
            return 0;
        }
        final ZLTextElementArea selectionStartArea = mySelection.getStartArea(myCurrentPage);
        if (selectionStartArea != null) {
            return selectionStartArea.YStart;
        }
        if (mySelection.hasPartBeforePage(myCurrentPage)) {
            final ZLTextElementArea firstArea = myCurrentPage.TextElementMap.getFirstArea();
            return firstArea != null ? firstArea.YStart : 0;
        } else {
            final ZLTextElementArea lastArea = myCurrentPage.TextElementMap.getLastArea();
            return lastArea != null ? lastArea.YEnd : 0;
        }
    }

    public int getSelectionEndY() {
        if (mySelection.isEmpty()) {
            return 0;
        }
        final ZLTextElementArea selectionEndArea = mySelection.getEndArea(myCurrentPage);
        if (selectionEndArea != null) {
            return selectionEndArea.YEnd;
        }
        if (mySelection.hasPartAfterPage(myCurrentPage)) {
            final ZLTextElementArea lastArea = myCurrentPage.TextElementMap.getLastArea();
            return lastArea != null ? lastArea.YEnd : 0;
        } else {
            final ZLTextElementArea firstArea = myCurrentPage.TextElementMap.getFirstArea();
            return firstArea != null ? firstArea.YStart : 0;
        }
    }

    public ZLTextPosition getSelectionStartPosition() {
        return mySelection.getStartPosition();
    }

    public ZLTextPosition getSelectionEndPosition() {
        return mySelection.getEndPosition();
    }

    public boolean isSelectionEmpty() {
        return mySelection.isEmpty();
    }

    public void resetRegionPointer() {
        mySelectedRegionSoul = null;
        myHighlightSelectedRegion = true;
    }

    public ZLTextRegion nextRegion(Direction direction, ZLTextRegion.Filter filter) {
        return myCurrentPage.TextElementMap.nextRegion(getSelectedRegion(), direction, filter);
    }

    @Override
    public boolean canScroll(PageIndex index) {
        switch (index) {
            default:
                return true;
            case next: {
                final ZLTextWordCursor cursor = getEndCursor();
                return cursor != null && !cursor.isNull() && !cursor.isEndOfText();
            }
            case previous: {
                final ZLTextWordCursor cursor = getStartCursor();
                return cursor != null && !cursor.isNull() && !cursor.isStartOfText();
            }
        }
    }

}
